// Skeletal program for the "Image Warper" assignment
// Written by:  Minglun Gong

/*
 * Modified by Daniel Cook
 * CS4751 (Graphics)
 * Assignment #2 - Beier-Neely Image Warping
 * 
 * Feb 10, 2014
 * 
 * Algorithm begins at line 83
 * Vec2D class added at line 258
*/

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;

// Main class
public class ImageWarp extends Frame implements ActionListener {
	ControlCanvas input, output;
	BufferedImage srcImage;
	int width, height;
	// Constructor
	public ImageWarp(String file) {
		super("Image Warper");
		// load image
		Image image = Toolkit.getDefaultToolkit().getImage(file);
		MediaTracker mt = new MediaTracker(this);
		try {
			mt.addImage(image, 0);
			mt.waitForID(0);
		}
		catch ( Exception e ) {
			e.printStackTrace();
		}
		// convert to buffered image
		width = image.getWidth(null);
		height = image.getHeight(null);
		srcImage = new BufferedImage
			(width, height, BufferedImage.TYPE_INT_RGB);
		srcImage.createGraphics().drawImage(image, 0, 0, null);
		// prepare the panel for input and output images.
		Panel main = new Panel();
		input = new ControlCanvas(image);
		output = new ControlCanvas(image);
		main.setLayout(new GridLayout(1, 2, 10, 10));
		main.add(input);
		main.add(output);
		// prepare the panel for buttons.
		Panel controls = new Panel();
		Button button = new Button("Clear Control Lines");
		button.addActionListener(this);
		controls.add(button);
		button = new Button("Warp Image");
		button.addActionListener(this);
		controls.add(button);
		// add two panels
		add("Center", main);
		add("South", controls);
		addWindowListener(new ExitListener());
	}
	class ExitListener extends WindowAdapter {
		public void windowClosing(WindowEvent e) {
			System.exit(0);
		}
	}

	// Action listener for buttons
	public void actionPerformed(ActionEvent e) {
		// clear control lines and restore the output panel
		if ( ((Button)e.getSource()).getLabel().equals("Clear Control Lines") ) {
			input.lines.clear();
			input.repaint();
			output.lines.clear();
			output.resetImage(srcImage);
			output.repaint();
		}
		if ( ((Button)e.getSource()).getLabel().equals("Warp Image") ) {
			int pixels[] = new int [width*height];
			
			//Added for assignment:
			//Image warping algorithm as described in class
			for ( int y=0, i=0; y<height; y++ ){
				for ( int x=0; x<width; x++, i++ ){
					Vec2D sumP = new Vec2D(0,0);
					double sumW = 0.0;
					
					for( int z=0; z<output.lines.size(); z++ ){
						Vec2D P = new Vec2D(x, y);
						Vec2D A = new Vec2D(output.lines.get(z).x0, output.lines.get(z).y0);
						Vec2D B = new Vec2D(output.lines.get(z).x1, output.lines.get(z).y1);
						
						//---Global to local conversion---//
						
						//Calculate u ratio
						Vec2D PA = P.subtract(A);
						Vec2D BA = B.subtract(A);
						
						double uDot = PA.dot(BA);
						double BALenSq = Math.pow(BA.length(), 2);
						
						double u = uDot / BALenSq;
						
						//Calculate v vector
						Vec2D D = new Vec2D(B.y-A.y, -(B.x-A.x));
						D = D.divideScalar(D.length());
						
						double v = PA.dot(D);
						
						//---Calculate weight---//
						double d = 0.0;
						if(u < 0.0){
							d = PA.length();
						}
						else if(u > 1){
							Vec2D PB = P.subtract(B);
							d = PB.length();
						}
						else{
							d = Math.abs(v);
						}
						
						double w = Math.pow(Math.pow(output.lines.get(z).length(), 1.0) / (0.0001 + d), 2);
						
						//---Local to global conversion---//
						Vec2D Aprime = new Vec2D(input.lines.get(z).x0, input.lines.get(z).y0);
						Vec2D Bprime = new Vec2D(input.lines.get(z).x1, input.lines.get(z).y1);
						
						Vec2D Dprime = new Vec2D(Bprime.y-Aprime.y, -(Bprime.x-Aprime.x));
						Dprime = Dprime.divideScalar(Dprime.length());
						
						Vec2D BAprime = Bprime.subtract(Aprime);
						Vec2D uBA = BAprime.multiplyScalar(u);
						Vec2D vD = Dprime.multiplyScalar(v);
						
						Vec2D Pprime = Aprime.add(uBA).add(vD);
						
						sumP = sumP.add(Pprime.multiplyScalar(w));
						sumW += w;
					}
					
					Vec2D Psrc = sumP.divideScalar(sumW);
					
					//Make sure Psrc is in range before we sample
					if((Math.floor(Psrc.x) < width) && (Math.floor(Psrc.x) >= 0) && (Math.floor(Psrc.y) < height) && (Math.floor(Psrc.y) >= 0)){
						pixels[i] = srcImage.getRGB((int)Math.floor(Psrc.x), (int)Math.floor(Psrc.y));
					}
					else{
						pixels[i] = 0;
					}
				}
			}
			//End of warping algorithm

			output.resetImage(createImage(new MemoryImageSource
				(width, height, pixels, 0, width)));
		}
	}
	public static void main(String[] args) {
		ImageWarp window = new ImageWarp(args.length>0 ? args[0] : "cabot_tower.png");
		window.setSize(750,350);
		window.setVisible(true);
	}
}

// Canvas for image display and control input
class ControlCanvas extends Canvas {
	int xoffset, yoffset;
	Image image;
	Vector<LineSegment> lines = new Vector<LineSegment>();
	LineSegment newline;
	// initialize the image and mouse control
	public ControlCanvas(Image input) {
		image = input;
		DragListener drag = new DragListener();
		addMouseListener(drag);
		addMouseMotionListener(drag);
	}
	// change the image and redraw the canvas
	public void resetImage(Image input) {
		image = input;
		repaint();
	}
	// maintain control line segment list
	public void createLineSegment(int x, int y) {
		newline = new LineSegment(x-xoffset, y-yoffset);
	}
	public void updateLineSegment(int x, int y) {
		Graphics g = getGraphics();
		g.setXORMode(Color.RED);
		newline.draw(g, xoffset, yoffset);
		newline.resetEndPos(x-xoffset, y-yoffset);
		newline.draw(g, xoffset, yoffset);
	}
	public void insertLineSegment(int x, int y) {
		lines.add(newline);
		Graphics g = getGraphics();
		g.setColor(Color.RED);
		newline.draw(g, xoffset, yoffset);
	}
	// redraw the canvas
	public void paint(Graphics g) {
		xoffset = (getWidth() - image.getWidth(null)) / 2;
		yoffset = (getHeight() - image.getHeight(null)) / 2;
		g.setColor(Color.LIGHT_GRAY);
		g.fillRect(0, 0, getWidth()-1, getHeight()-1);
		g.drawImage(image, xoffset, yoffset, this);
		g.setColor(Color.RED);
		for ( int i=0; i<lines.size(); i++ )
			lines.elementAt(i).draw(g, xoffset, yoffset);
	}

	// Action listener for mouse
	class DragListener extends MouseAdapter implements MouseMotionListener {
		public void mousePressed(MouseEvent e) {
			createLineSegment(e.getX(), e.getY());
		}
		public void mouseReleased(MouseEvent e) {
			insertLineSegment(e.getX(), e.getY());
		}
		public void mouseMoved(MouseEvent e) {}
		public void mouseDragged(MouseEvent e) {
			updateLineSegment(e.getX(), e.getY());
		}
	}
}

// LineSegment class defines the control line
class LineSegment {
	int x0, y0, x1, y1;
	// Constructor
	public LineSegment(int x, int y) {
		x0 = x1 = x;
		y0 = y1 = y;
	}
	public void resetEndPos(int x, int y) {
		x1 = x; y1 = y;
	}
	public void draw(Graphics g, int xoffset, int yoffset) {
		g.drawLine(x0+xoffset, y0+yoffset, x1+xoffset, y1+yoffset);
	}
	
	//NEW
	//Convienience method to return length of a line segment
	public double length(){
		Vec2D a = new Vec2D(this.x0, this.y0);
		Vec2D b = new Vec2D(this.x1, this.y1);
		Vec2D v = b.subtract(a);
		
		return v.length();
	}
}

//NEW for assignment
//Class representing a 2D vector, implements various vector math functions
class Vec2D {
	double x, y;
	
	public Vec2D(double x, double y){
		this.x = x;
		this.y = y;
	}
	
	public Vec2D add(Vec2D b){
		return new Vec2D(this.x + b.x, this.y + b.y);
	}
	
	public Vec2D subtract(Vec2D b){
		return new Vec2D(this.x - b.x, this.y - b.y);
	}
	
	public Vec2D multiply(Vec2D b){
		return new Vec2D(this.x*b.x, this.y*b.y);
	}
	
	public Vec2D divideScalar(double b){
		return new Vec2D(this.x/b, this.y/b);
	}
	
	public Vec2D addScalar(double b){
		return new Vec2D(this.x+b, this.y+b);
	}
	
	public Vec2D multiplyScalar(double b){
		return new Vec2D(this.x*b, this.y*b);
	}
	
	public double dot(Vec2D b){
		return ((this.x*b.x) + (this.y*b.y));
	}
	
	public double length(){
		return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
	}
	
}
